/*
 * Copyright 2013 JROSE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jrose.core.jdbc.entity;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jrose.api.annotation.persistence.Table;
import org.jrose.api.common.IterableFilter;
import org.jrose.core.jdbc.entity.field.PersistenceField;
import org.jrose.core.jdbc.entity.field.PersistenceFieldFactory;
import org.jrose.core.jdbc.entity.field.internal.ColumnField;
import org.jrose.core.jdbc.entity.field.internal.CreateTimestampField;
import org.jrose.core.jdbc.entity.field.internal.PrimaryKeyField;
import org.jrose.core.jdbc.entity.field.internal.RevisionField;
import org.jrose.core.jdbc.entity.field.internal.UpdateTimestampField;
import org.jrose.core.util.AnnotationUtils;
import org.jrose.core.util.CollectionUtils;
import org.jrose.core.util.ReflectionUtils;
import org.springframework.util.Assert;

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Default {@link EntityDefinition} implementation.
 *
 * @author Xiaohai Zhang
 * @since 2013-08-22 10:23
 */
@Slf4j
class DefaultEntityDefinition<E extends Serializable> implements EntityDefinition<E> {
    @Getter private final Class<E> entityClass;
    @Getter private final String tableName;
    @Getter private final List<PrimaryKeyField> primaryKeyFields = new LinkedList<>();
    @Getter private final List<CreateTimestampField> createTimestampFields = new LinkedList<>();
    @Getter private final List<UpdateTimestampField> updateTimestampFields = new LinkedList<>();
    @Getter private final List<RevisionField> revisionFields = new LinkedList<>();
    @Getter private final List<ColumnField> columnFields = new LinkedList<>();
    @Getter private final List<PersistenceField> allFields = new LinkedList<>();
    @Getter private final Map<String, PersistenceField> allFieldsMap = new LinkedHashMap<>();
    @Getter private final Map<String, PersistenceField> allColumnsMap = new LinkedHashMap<>();

    @Override
    public boolean hasPrimaryKey() {
        return !primaryKeyFields.isEmpty();
    }

    @Override
    public PersistenceField getPersistenceField(String fieldName) {
        return allFieldsMap.get(fieldName);
    }

    @Override
    public PersistenceField getPersistenceFieldByColumn(String columnName) {
        return allColumnsMap.get(columnName);
    }

    DefaultEntityDefinition(Class<E> entityClass) {
        Assert.notNull(entityClass, "Entity class must not be null");

        this.entityClass = entityClass;

        Table table = AnnotationUtils.getAnnotation(entityClass, Table.class);
        Assert.notNull(table);
        this.tableName = table.value();
        Assert.notNull(tableName);

        this.parseEntityFields();

        // Validate the AUTO_FILE field count must not be greater than 1
        int autoIncFieldCount = 0;
        for (PersistenceField field : allFields) {
            if (field.isAutoIncField()) {
                autoIncFieldCount += 1;
                Assert.isTrue(autoIncFieldCount <= 1);
            }
        }
        // Validate primary key index not duplicated
        if (primaryKeyFields.size() > 1) {
            Set<Integer> indexes = new HashSet<>();
            for (PrimaryKeyField primaryKeyField : primaryKeyFields) {
                Assert.isTrue(!indexes.contains(primaryKeyField.getAnnotation().index()));
                indexes.add(primaryKeyField.getAnnotation().index());
            }
        }
    }

    // private methods:

    private void parseEntityFields() {
        List<ReflectionUtils.ClassField> classFields = ReflectionUtils.getAllFields(entityClass);
        classFields = CollectionUtils.filterIterable(classFields, new IterableFilter<ReflectionUtils.ClassField>() {
            @Override
            public boolean accept(ReflectionUtils.ClassField element) {
                return !element.isStaticField() && !element.isFinalField();
            }
        });
        for (ReflectionUtils.ClassField classField : classFields) {
            PersistenceField field = PersistenceFieldFactory.build(classField.getClazz(), classField.getField());
            if (field == null) {
                continue;   // no available field annotation found, ignore
            }
            if (field.getClass() == PrimaryKeyField.class) {
                this.primaryKeyFields.add((PrimaryKeyField) field);
            } else if (field.getClass() == CreateTimestampField.class) {
                this.createTimestampFields.add((CreateTimestampField) field);
            } else if (field.getClass() == UpdateTimestampField.class) {
                this.updateTimestampFields.add((UpdateTimestampField) field);
            } else if (field.getClass() == RevisionField.class) {
                this.revisionFields.add((RevisionField) field);
            } else if (field.getClass() == ColumnField.class) {
                this.columnFields.add((ColumnField) field);
            } else {
                throw new UnsupportedOperationException();
            }
        }

        // sort primary key fields if necessary
        if (this.primaryKeyFields.size() > 1) {
            Collections.sort(this.primaryKeyFields, new Comparator<PrimaryKeyField>() {
                @Override
                public int compare(PrimaryKeyField o1, PrimaryKeyField o2) {
                    Integer idx1 = o1.getAnnotation().index();
                    Integer idx2 = o2.getAnnotation().index();
                    return idx1.compareTo(idx2);
                }
            });
        }

        this.allFields.addAll(this.primaryKeyFields);
        this.allFields.addAll(this.createTimestampFields);
        this.allFields.addAll(this.updateTimestampFields);
        this.allFields.addAll(this.revisionFields);
        this.allFields.addAll(this.columnFields);

        for (PersistenceField field : this.allFields) {
            this.allFieldsMap.put(field.getFieldName(), field);
            this.allColumnsMap.put(field.getColumnName(), field);
        }

        Assert.isTrue(allFieldsMap.size() == allColumnsMap.size(), "Duplicated column name found");
    }
}
